<?php

	namespace org\tekuna\core\plugin;

	use \InvalidArgumentException;
	
	use org\tekuna\base\Tekuna;
	
	use org\tekuna\core\configuration\Configuration;
	use org\tekuna\core\configuration\ConfigurationElement;
	use org\tekuna\core\configuration\ConfigurationException;
	use org\tekuna\core\event\Event;
	use org\tekuna\core\event\AbstractEventListener;
	

	class PluginManager extends AbstractEventListener {
		
		protected
			$arrPlugins = array();
		
		
		public function loadPlugins(Configuration $objApplicationConfig) {
			
			// load all plugin descriptors
			$this -> loadPluginDescriptors($objApplicationConfig -> getRootElement());
			
			// check plugin compatibility with the framework version
			$this -> checkPluginCompatibilities();
			
			// check plugin dependencies
			$this -> checkPluginDependencies();
						
			// import plugin configs
			$this -> importPluginConfigs($objApplicationConfig);
			
			// add class loaders
			$this -> registerPluginClassLoaders();
			
			
//echo '<h1>true</h1>';
//var_dump(self :: versionCompatible('1', '1'));			
//var_dump(self :: versionCompatible('1.1', '1'));			
//var_dump(self :: versionCompatible('1.1', '1.1'));			
//var_dump(self :: versionCompatible('1.1.1', '1'));
//var_dump(self :: versionCompatible('1.1.1', '1.1'));
//var_dump(self :: versionCompatible('1.1.1', '1.1.1'));
//var_dump(self :: versionCompatible('1.1.123', '1'));
//var_dump(self :: versionCompatible('1.1.123', '1.1'));
//var_dump(self :: versionCompatible('1.1.123', '1.1.123'));
//
//echo '<h1>false</h1>';
//var_dump(self :: versionCompatible('2', '1'));			
//var_dump(self :: versionCompatible('2', '2.2'));			
//var_dump(self :: versionCompatible('2', '2.2.2'));			
//var_dump(self :: versionCompatible('2', '3'));			
//var_dump(self :: versionCompatible('2.2', '1'));			
//var_dump(self :: versionCompatible('2.2', '1.2'));			
//var_dump(self :: versionCompatible('2.2', '1.2.3'));			
//var_dump(self :: versionCompatible('2.2', '2.2.2'));			
//var_dump(self :: versionCompatible('2.2', '3'));			
//var_dump(self :: versionCompatible('2.2', '3.4'));			
//var_dump(self :: versionCompatible('2.2', '3.4.5'));			
//var_dump(self :: versionCompatible('2.2', '2.2.2'));			
//var_dump(self :: versionCompatible('2.2.2', '1'));			
//var_dump(self :: versionCompatible('2.2.2', '1.2'));			
//var_dump(self :: versionCompatible('2.2.2', '1.2.3'));			
//var_dump(self :: versionCompatible('2.2.2', '3'));			
//var_dump(self :: versionCompatible('2.2.2', '3.2'));			
//var_dump(self :: versionCompatible('2.2.2', '3.2.1'));			
//var_dump(self :: versionCompatible('1.1.123', '1.1.12'));
//die;
#		self :: versionCompatible('1.1.123', '1.1.12d');
		}
		
		
		public function getPlugins() {
			
			return $this -> arrPlugins;
		}
		
		
		public function getPlugin($sPluginKey, $bThrowExceptionIfUnknown = true) {
			
			foreach ($this -> arrPlugins as $objPlugin) {
				
				if ($objPlugin -> getKey() === $sPluginKey) {
					
					return $objPlugin;
				}
			}
			
			if ($bThrowExceptionIfUnknown) {
			
				throw new PluginException("The plugin '$sPluginKey' does not exist.");
			}
		}
		
		
		public static function versionCompatible($sHostVersion, $sArtifactVersion) {
			
			$sHostVersion = trim($sHostVersion);
			$sArtifactVersion = trim($sArtifactVersion);
			
			$sValidVersionPattern = '~^\d+(\.\d+)*$~';
			if (! preg_match($sValidVersionPattern, $sHostVersion)) {
				
				throw new InvalidArgumentException("The version '". $sHostVersion ."' does not match the required pattern '". $sValidVersionPattern ."'.");
			}
			
			if (! preg_match($sValidVersionPattern, $sArtifactVersion)) {
				
				throw new InvalidArgumentException("The version '". $sArtifactVersion ."' does not match the required pattern '". $sValidVersionPattern ."'.");
			}
			
			#echo "<br><br>version compatible ($sHostVersion, $sArtifactVersion)?";

			// the artifact version is like a pattern that the host version must meet
			$sArtifactVersionPattern = '~^'. str_replace('.', '\\.', $sArtifactVersion) .'(\.\d+)*$~';

			// return true if the pattern matches the host version
			return (boolean) preg_match($sArtifactVersionPattern, $sHostVersion);
		}
		
		
		protected function loadPluginDescriptors(ConfigurationElement $objRootElement) {
			
			$arrPluginElements = $objRootElement -> getAllChildElements('core', 'plugin');
			
			foreach ($arrPluginElements as $objPluginElement) {
				
				$objNewPlugin = NULL;
				
				if ($objPluginElement -> hasAttribute('core', 'phar')) {
					
					$sPluginPhar = $objPluginElement -> getAttribute('core', 'phar') -> getValue();
					$objNewPlugin = new PharBasedPlugin($sPluginPhar);
				}
				
				if ($objPluginElement -> hasAttribute('core', 'dir')) {
					
					$sPluginDir = $objPluginElement -> getAttribute('core', 'dir') -> getValue();
					$objNewPlugin = new DirectoryBasedPlugin($sPluginDir);
				}
				
				if ($objNewPlugin !== NULL) {
					
					if ($this -> getPlugin($objNewPlugin -> getKey(), false) !== NULL) {
						
						throw new PluginException("The plugin '". $objNewPlugin -> getKey() ."' is referenced more than once. Each plugin can be loaded only once.");
					}
					
					$objNewPlugin -> setConfigurationElement($objPluginElement);
					$this -> arrPlugins[] = $objNewPlugin;
				}
				else {
					
					throw new ConfigurationException("Plugin not loadable. Neither 'phar' nor 'dir' attribute given.");
				}
			}
		}
		
		
		protected function checkPluginCompatibilities() {
			
			foreach ($this -> arrPlugins as $objPlugin) {
				
				$sTekunaVersion = Tekuna :: TEKUNA_VERSION;
				$sRequiredTekunaVersion = $objPlugin -> getTekunaVersion();
				
				if (! self :: versionCompatible($sTekunaVersion, $sRequiredTekunaVersion)) {
					
					$sPluginKey = $objPlugin -> getKey();
					$sPluginVersion = $objPlugin -> getVersion();
					
					throw new PluginException("The plugin '$sPluginKey' version '$sPluginVersion' requires Tekuna version '$sRequiredTekunaVersion' but trying to run on Tekuna version '$sTekunaVersion'.");
				}
			}
		}
		
		
		protected function checkPluginDependencies() {
			
			foreach ($this -> arrPlugins as $objPlugin) {
				
				$sPluginKey = $objPlugin -> getKey();
				
				$arrPluginDependencies = $objPlugin -> getPluginDependencies();
				foreach ($arrPluginDependencies as $sDependentPluginKey => $sDependentPluginVersion) {
					
					try {
						
						$objDependentPlugin = $this -> getPlugin($sDependentPluginKey);
						$sLoadedDependentPluginVersion = $objDependentPlugin -> getVersion();
					}
					catch (PluginException $objException) {
						
						throw new PluginException("Plugin '$sPluginKey' requires plugin '$sDependentPluginKey' which is not available.", -1, $objException);
					}

					if (! self :: versionCompatible($sLoadedDependentPluginVersion, $sDependentPluginVersion)) {
						
						throw new PluginException("The Plugin '$sPluginKey' requires plugin '$sDependentPluginKey' in version '$sDependentPluginVersion', but available version is '$sLoadedDependentPluginVersion'.");
					}
				}
			}
		}
		
		
		protected function importPluginConfigs(Configuration $objApplicationConfig) {
			
			$objApplicationRootElement = $objApplicationConfig -> getRootElement();
			
			foreach ($this -> arrPlugins as $objPlugin) {
				
				// the the plugin's config root element
				$objPluginRootElement = $objPlugin -> getPluginConfig() -> getRootElement();
				
				// import all elements from plugin config to application config
				// right after the plugin definition element
				$objPreviousElement = $objPlugin -> getConfigurationElement();
				foreach ($objPluginRootElement -> getChildElements() as $objChildElement) {

					$objApplicationRootElement -> insertChildElementAfter($objChildElement, $objPreviousElement);
					$objPreviousElement = $objChildElement;
				}
			}
		}
		
		
		protected function registerPluginClassLoaders() {
			
			foreach ($this -> arrPlugins as $objPlugin) {
				
				Tekuna :: getInstance() -> addClassLoader($objPlugin -> getPluginClassLoader());
			}
		}
		
		
		public function handlesEvent(Event $objEvent) {
			
			// do it in that way to avoid dependency from core to framework package
			return get_class($objEvent) === 'org\\tekuna\\framework\\info\\InformationEvent';
		}

	
		public function handleEvent(Event $objEvent) {
			
			$sOutput = '';
			foreach ($this -> arrPlugins as $objPlugin) {
				
				$sOutput .= $objPlugin -> getKey() .' ('. $objPlugin -> getVersion() .")\n";
				$sOutput .= "   requires Tekuna (". $objPlugin -> getTekunaVersion() .")\n";
				
				foreach ($objPlugin -> getPluginDependencies() as $sDependentPlugin => $sDependentPluginVersion) {
					
					$sOutput .= "   requires $sDependentPlugin ($sDependentPluginVersion)\n";
				}
				
				$sOutput .= "\n";
			}
			
			$objEvent -> addInfoSection('Plugins', $sOutput);
		}
	}
